﻿using Decisions.HL7.DataTypes;
using Decisions.HL7.Message;
using DecisionsFramework.Design.Properties;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;

namespace Decisions.HL7.Parser
{
    /**
     *  This class is at the heart of Decisions HL7 Flow Engine.
     *  It takes a text entry and creates HL7 from it.
     */
    public abstract class HL7Parser
    {
        public static Envelope Parse(string hl7Text) {

            return Parse(hl7Text, HL7ParserOptions.DefaultHL7ParserOptions);
        
        }
        
        public static Envelope Parse(string hl7Text, HL7ParserOptions options) {

            Envelope resultEnvelope = new Envelope();

            if (options.CleanAndTrimLineStarts)
            {
                hl7Text = CleanLineStarts(hl7Text);
            }
            
            List<string[]> messageArray = SplitTextAlongMessages(hl7Text);

            resultEnvelope.RawMessageCount = messageArray.Count;

            foreach (string[] messageSegmentArray in messageArray) {
                resultEnvelope.AddMessage(ParseTextIntoHL7Message(messageSegmentArray, options));
            }

            return resultEnvelope;

        }

        private static AbstractMessage ParseTextIntoHL7Message(string[] messageSegmentArray, HL7ParserOptions options)
        {
            int indexOfMSH = messageSegmentArray[0].IndexOf(HL7ParserConstants.MSH_START, StringComparison.OrdinalIgnoreCase);
            if (indexOfMSH > 10 || indexOfMSH < 0) // MSH must be in the first few characters for this to be valid.
            {
                throw new HL7ParserException("Message does not start with MSH and cannot be parsed.");
            }
            // This string[] represents any number of hl7 message segmenets.
            MSH messageHeader = ParseMSH(messageSegmentArray[0].Substring(indexOfMSH), options); // Apply fix to any preceeding bad chars
            options = ConfigureOptionsFromMSH(messageHeader, options);

            string[] mshPieces = messageSegmentArray[0].Split(new char[] { options.FieldSep }, StringSplitOptions.None);
            options.Version = GetVersionFromMSHText(mshPieces);

            // If version is 2.1 or 2.2 switch parser to 2.3 because we don't generate support for 2.1. or 2.2
            if (options.Version == HL7Version.v21) {
                options.Version = HL7Version.v23;
            }
            if (options.Version == HL7Version.v22)
            {
                options.Version = HL7Version.v23;
            }

            // Now that we have the MSH we can construct the right message.
            AbstractMessage newMessage = CreateMessageFromType(messageHeader);

            try
            {
                newMessage.MessageType = (HL7Message)Enum.Parse(typeof(HL7Message), messageHeader.MessageType.MessageCode + "_" + messageHeader.MessageType.TriggerEvent);
            }
            catch (Exception ex) {
                newMessage.AddParserMessage("Could not set MessageType on message from the MSH segment values due to an exception: " + ex.ToString());
            }

            ParseSegmentsIntoMessage(newMessage, messageSegmentArray, options);

            return newMessage;
        }

        public static HL7Version GetVersionFromMSHText(string[] mshText)
        {
            // Whe nparsing the MSH we always need to get the HL7 Version number from the message
            // header in order to start parsin and we can always rely on it being the 
            // MSH 11
            return GetVersionFromString(mshText[11]);
        }

        public static HL7Version GetVersionFromMSH(MSH MSH)
        {
            return GetVersionFromString(MSH.VersionID.VersionID);
        }

        private static void ParseSegmentsIntoMessage(AbstractMessage newMessage, string[] messageSegmentArray, HL7ParserOptions options)
        {
            // Assume the segments are 100% in order.
            // Sorting comes later.

            Stack<string> availableData = new Stack<string>();
           
            // Skip the MSH - Reverse the Segments
            // Put on the stack and send through the message.
            foreach (string eachSegment in messageSegmentArray.Skip(1).Reverse()) {
                availableData.Push(eachSegment);
            }

            Stack<string> leftOverData = newMessage.ConsumeTextSegments(availableData, options);

            foreach (string s in leftOverData) {
                newMessage.AddParserMessage("Data Left Over (Possibly Out of Order or not in version) " + s);
            }

        }

        private static AbstractMessage CreateMessageFromType(MSH messageHeader)
        {
            string typeName = string.Format("Decisions.HL7.Message.{0}_{1}", messageHeader.MessageType.MessageCode, messageHeader.MessageType.TriggerEvent);

            object result = Activator.CreateInstance(TypeCreatorRegistry.Creator.GetTypeForName(typeName));
            
            if ((result is AbstractMessage) == false) {
                throw new Exception(string.Format("Tried to create message {0} but it is not an AbstractMessage", typeName));
            }
            ((AbstractMessage)result).MSH = messageHeader;
            return (AbstractMessage)result;
        }

        private static HL7ParserOptions ConfigureOptionsFromMSH(MSH messageHeader, HL7ParserOptions options)
        {
            options.FieldSep = messageHeader.FieldSeparator.ToCharArray()[0];

            char[] chars = messageHeader.EncodingCharacters.ToCharArray();
            options.ComponentSep = chars[0];
            options.FieldRepeatSep = chars[1];
            options.EscapeChar = chars[2];
            options.SubComponentSep = chars[3];

            return options;
        }

        private static MSH ParseMSH(string mshSegment, HL7ParserOptions options)
        {
            // MSH is very special because the character right after the MSH
            // is the field separator and must be used for the rest of the message.
            if (string.IsNullOrEmpty(mshSegment) == true || mshSegment.Length < 4) {
                throw new Exception("This MSH segment is not valid and cannot be parsed.");
            }

            int indexOfMSH = mshSegment.IndexOf(HL7ParserConstants.MSH_START);
            if (indexOfMSH + 3 > mshSegment.Length) {
                throw new Exception("This MSH segment is not valid and cannot be parsed.  There is no field separator after the MSH header.");
            }

            char[] mshSegmentAsCharArray = mshSegment.ToCharArray();
            options.FieldSep = mshSegmentAsCharArray[indexOfMSH + 3];

            // Can we set the encoding chars too?
            if (indexOfMSH + 4 + 4 < mshSegment.Length)
            {
                // if the encoding chars can't be set we don't really care because 
                // the defaults are sensible.
                options.ComponentSep = mshSegmentAsCharArray[indexOfMSH + 4];
                options.FieldRepeatSep = mshSegmentAsCharArray[indexOfMSH + 5];
                options.EscapeChar = mshSegmentAsCharArray[indexOfMSH + 6];
                options.SubComponentSep = mshSegmentAsCharArray[indexOfMSH + 7];
            }

            MSH newMSH = new MSH();
            newMSH.ConsumeTextSegment(mshSegment, options);
            return newMSH;
        }

        public static HL7Version GetVersionFromString(string hl7VersionNumber)
        {
            hl7VersionNumber = hl7VersionNumber.Replace(".", string.Empty);
            if (hl7VersionNumber.StartsWith("v", StringComparison.OrdinalIgnoreCase) == false) {
                hl7VersionNumber = "v" + hl7VersionNumber;
            }

            return (HL7Version)Enum.Parse(typeof(HL7Version), hl7VersionNumber);
        }

        private static string CleanLineStarts(string hl7Text)
        {
            string[] allParts = SplitTextByNewLineOrCarriageReturn(hl7Text);

            for (int i = 0; i < allParts.Length; i++) {
                allParts[i] = CleanLineStart(allParts[i]);
            }

            return string.Join("\r", allParts);
        }

        /**
         * This line strips unintended hex chars, invisible unicode,
         * and other undesirable characters from the beginning of a line
         * of text in an HL7 message.
         */
        public static string CleanLineStart(string hl7MessageLine)
        {
            // Take this line and substring it down to the first three alpha chars and a pipe.
            Match segmentStartMatch = HL7ParserConstants.SEGMENT_START.Match(hl7MessageLine);
            if (segmentStartMatch.Success && segmentStartMatch.Index != 0) {
                hl7MessageLine.Substring(segmentStartMatch.Index);
            }
            return hl7MessageLine;
        }

        private static List<string[]> SplitTextAlongMessages(string hl7Text)
        {
            List<string[]> allMessages = new List<string[]>();
            string[] messagesSplit = SplitTextByNewLineOrCarriageReturn(hl7Text);

            List<string> messageParts = new List<string>();

            foreach (string eachLine in messagesSplit) {

                if (eachLine.StartsWith(HL7ParserConstants.MSH_START, StringComparison.OrdinalIgnoreCase)) { 
                    // We have a new message starting.
                    if (messageParts.Count > 0) { 
                        allMessages.Add(messageParts.ToArray());
                        messageParts.Clear();
                    }
                }
                messageParts.Add(eachLine);
            }

            // Once complete... add the last one.
            allMessages.Add(messageParts.ToArray());

            return allMessages;
        }

        private static string[] SplitTextByNewLineOrCarriageReturn(string hl7Text)
        {
            return hl7Text.Split(HL7ParserConstants.MESSAGE_SEPARATORS, StringSplitOptions.RemoveEmptyEntries);
        }

        static HL7Parser() {
            
            // Register parsers by type if needed

        }

    }
}
